# 广发证券双均线测试代码

import backtrader as bt
import backtrader.feeds as btfeeds
import pandas as pd
import tushare as ts
import matplotlib.pyplot as plt
from datetime import datetime

plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False


def get_stock_data(stock_code, start_date, end_date):
    ts.set_token('63defece641436396989405961579b92948c7f5598523913b3754e6a')
    pro = ts.pro_api()
    
    # 获取日线数据
    df = pro.daily(
        ts_code=stock_code,
        start_date=start_date,
        end_date=end_date,
        fields='trade_date,open,high,low,close,vol'
    )
    
    # 数据预处理：转换日期格式、调整列顺序（Backtrader要求的顺序）
    df['trade_date'] = pd.to_datetime(df['trade_date'], format='%Y%m%d')
    df = df.sort_values('trade_date')  # 按日期升序排列
    df.rename(
        columns={
            'trade_date': 'datetime',
            'vol': 'volume'
        },
        inplace=True
    )
    # 调整列顺序为 Backtrader 要求的 [datetime, open, high, low, close, volume]
    df = df[['datetime', 'open', 'high', 'low', 'close', 'volume']]
    
    return df

# 2. 定义双均线策略
class DoubleMAStrategy(bt.Strategy):
    """
    双均线策略：
    - 短期均线（如5日）上穿长期均线（如20日）时，买入（金叉）
    - 短期均线下穿长期均线时，卖出（死叉）
    - 每次交易使用固定仓位（如100股）
    """
    # 策略参数（可在回测时调整）
    params = (
        ('short_period', 5),   # 短期均线周期
        ('long_period', 20),   # 长期均线周期
        ('order_size', 100),   # 每次交易股数
    )

    def __init__(self):
        # 初始化均线指标
        self.short_ma = bt.indicators.SimpleMovingAverage(
            self.data.close, period=self.params.short_period, plotname='短期均线'
        )
        self.long_ma = bt.indicators.SimpleMovingAverage(
            self.data.close, period=self.params.long_period, plotname='长期均线'
        )
        
        # 交叉信号检测（金叉：短期均线上穿长期均线；死叉：短期均线下穿长期均线）
        self.gold_cross = bt.indicators.CrossOver(self.short_ma, self.long_ma)
        self.death_cross = bt.indicators.CrossOver(self.long_ma, self.short_ma)
        
        # 记录当前持仓状态（0：空仓，1：持仓）
        self.position_status = 0

    def next(self):
        """
        每根K线执行一次（核心交易逻辑）
        """
        # 金叉且空仓时，买入
        if self.gold_cross[0] == 1 and self.position_status == 0:
            # 发出买入订单（市价单，买入指定股数）
            self.buy(size=self.params.order_size)
            self.position_status = 1  # 更新持仓状态为持仓
            print(f"买入信号：{self.data.datetime.date(0)} | 收盘价：{self.data.close[0]:.2f}")
        
        # 死叉且持仓时，卖出
        elif self.death_cross[0] == 1 and self.position_status == 1:
            # 发出卖出订单（市价单，卖出全部持仓）
            self.sell(size=self.params.order_size)
            self.position_status = 0  # 更新持仓状态为空仓
            print(f"卖出信号：{self.data.datetime.date(0)} | 收盘价：{self.data.close[0]:.2f}")

# 3. 回测执行函数
def run_backtest(stock_code, start_date, end_date, initial_cash=100000):
    # 1. 创建回测引擎
    cerebro = bt.Cerebro()
    
    # 2. 设置初始资金
    cerebro.broker.setcash(initial_cash)
    
    # 3. 设置交易手续费（0.1%佣金，最低5元，卖出时加收0.1%印花税）
    cerebro.broker.setcommission(
        commission=0.0013,  # 佣金率
        stocklike=True,
        percabs=False
    )
    
    # 4. 获取并加载数据
    stock_data = get_stock_data(stock_code, start_date, end_date)
    # 转换为Backtrader数据格式
    data_feed = btfeeds.PandasData(
        dataname=stock_data,
        datetime='datetime',
        open='open',
        high='high',
        low='low',
        close='close',
        volume='volume',
        openinterest=None  # A股无持仓兴趣数据，设为None
    )
    cerebro.adddata(data_feed, name=stock_code)
    
    # 5. 添加策略（可传入参数调整均线周期）
    cerebro.addstrategy(DoubleMAStrategy, short_period=5, long_period=20)
    
    # 6. 添加分析指标（回测结果评估）
    # 收益率分析
    cerebro.addanalyzer(bt.analyzers.Returns, _name='returns')
    # 夏普比率（风险调整后收益，无风险利率默认0.02）
    cerebro.addanalyzer(bt.analyzers.SharpeRatio, _name='sharpe', riskfreerate=0.02)
    # 最大回撤（风险指标）
    cerebro.addanalyzer(bt.analyzers.DrawDown, _name='drawdown')
    # 交易统计（胜率、盈亏比等）
    cerebro.addanalyzer(bt.analyzers.TradeAnalyzer, _name='trade_analyzer')
    
    # 7. 运行回测
    print(f"\n回测开始：初始资金 {initial_cash:.2f} 元")
    results = cerebro.run()
    strategy = results[0]  # 获取策略实例
    
    # 8. 输出回测结果
    final_cash = cerebro.broker.getvalue()
    print(f"回测结束：最终资金 {final_cash:.2f} 元")
    print(f"总收益率：{(final_cash / initial_cash - 1) * 100:.2f}%")
    
    returns = strategy.analyzers.returns.get_analysis()
    annual_return = returns.get('rnorm100', 0)  # 年化收益率（百分比形式，无需再×100）
    # 2. 夏普比率分析器（SharpeRatio）的标准字段
    sharpe = strategy.analyzers.sharpe.get_analysis()
    sharpe_ratio = sharpe.get('sharperatio', 0)  # 夏普比率
    # 3. 最大回撤分析器（DrawDown）的标准字段
    drawdown = strategy.analyzers.drawdown.get_analysis()
    max_drawdown = drawdown.get('max', {}).get('drawdown', 0)  # 最大回撤（百分比形式）
    # 4. 交易统计分析器（TradeAnalyzer）的标准字段
    trade_analyzer = strategy.analyzers.trade_analyzer.get_analysis()
    total_trades = trade_analyzer.get('total', {}).get('total', 0)  # 总交易次数
    win_trades = trade_analyzer.get('won', {}).get('total', 0)  # 盈利交易次数
    win_rate = (win_trades / total_trades * 100) if total_trades > 0 else 0  # 胜率
    profit_loss_ratio = trade_analyzer.get('won', {}).get('pnl', {}).get('total', 0) / \
                       abs(trade_analyzer.get('lost', {}).get('pnl', {}).get('total', 1)) if trade_analyzer.get('lost', {}).get('total', 0) > 0 else 0  # 盈亏比

    # 输出修复后的回测指标
    print(f"\n=== 回测指标详情 ===")
    print(f"年化收益率：{annual_return:.2f}%")  # 注意：rnorm100已是百分比，无需×100
    print(f"夏普比率：{sharpe_ratio:.2f}")
    print(f"最大回撤：{max_drawdown:.2f}%")
    print(f"总交易次数：{total_trades}")
    print(f"胜率：{win_rate:.2f}%")
    print(f"盈亏比：{profit_loss_ratio:.2f}")
    
    # 9. 绘制回测图表（价格、均线、交易信号）
    # cerebro.plot(
    #     style='candlestick',  # K线图样式
    #     title=f'{stock_code} 双均线策略回测结果',
    #     iplot=False  # 本地显示（非Jupyter环境）
    # )
    # plt.show()

# 4. 主函数（执行回测）
if __name__ == "__main__":
    # 回测参数设置（示例：贵州茅台，2020-2023年数据）
    STOCK_CODE = '000776.SZ'  # 股票代码（A股格式：60XXXX.SH 或 00XXXX.SZ）
    START_DATE = '20250101'    # 回测开始日期（YYYYMMDD）
    END_DATE = '20251231'      # 回测结束日期（YYYYMMDD）
    INITIAL_CASH = 100000      # 初始资金（元）
    
    # 执行回测
    run_backtest(stock_code=STOCK_CODE, start_date=START_DATE, end_date=END_DATE, initial_cash=INITIAL_CASH)
    